﻿
namespace Patterns4Net.Core.CecilExtensions
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using Mono.Cecil;
    using Mono.Cecil.Rocks;

    public static class TypeExtensions
    {
        private const string CannotResolveMessage = 
            "Cannot resolve type {0} which is a base type of {1}. " + 
            "This may happen when assembly that contains {0} is not loaded by Cecil. ";

        #region Walking the types hierarchy

        /// <summary>
        /// Returns all types from given module that have <paramref name="type"/> as a base type.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="module">The module as a scope for a search. Usually type.Module</param>
        public static IEnumerable<TypeReference> GetChildTypes(this TypeReference type, ModuleDefinition module)
        {
            Contract.Requires(type != null);
            Contract.Ensures(Contract.Result<IEnumerable<TypeReference>>() != null);
            return from t in module.GetAllTypes()
                   where (t.BaseType != null && t.BaseType.IsEqual(type)) ||
                        (t.HasInterfaces && t.Interfaces.Any(i => i.IsEqual(type)))
                   select t;
        }

        /// <summary>
        /// Traverses the class hierarchy and returns all base types of given type.
        /// </summary>
        public static IEnumerable<TypeReference> GetBaseTypes(this TypeReference type)
        {
            return GetBaseTypesExtension.GetBaseTypesAndIFaces(type, true, false);
        }

        /// <summary>
        /// Traverses the class hierarchy and returns all interfaces that given type implements.
        /// <see cref="TypeDefinition.Interfaces"/> returns only directly implemented interfaces, 
        /// but this method also returns interfaces implemented by base classes.
        /// </summary>
        public static IEnumerable<TypeReference> GetBaseTypesAndInterfaces(this TypeReference type)
        {
            return GetBaseTypesExtension.GetBaseTypesAndIFaces(type, true, true);
        }

        /// <summary>
        /// Returns all interfaces implemented by given type or by any of it's base classes or interfaces.
        /// </summary>
        public static IEnumerable<TypeReference> GetAllInterfaces(this TypeReference type)
        {
            return GetBaseTypesExtension.GetBaseTypesAndIFaces(type, false, true);
        }

        /// <summary>
        /// Returns true if the statement <c>left = right</c> would be considered valid by compiler. 
        /// In other words if left is a subtype of right.
        /// </summary>
        public static bool IsAssignableTo(this TypeReference right, TypeReference left)
        {
            return GetBaseTypesExtension.GetBaseTypesAndIFaces(left, true, true).Any(x => x.IsEqual(right));
        }

        /// <summary>
        /// Returns true if the statement <c>left = right</c> would be considered valid by compiler. 
        /// In other words if left is a subtype of right.
        /// </summary>
        public static bool IsAssignableFrom(this TypeDefinition left, TypeReference right)
        {
            return GetBaseTypesExtension.GetBaseTypesAndIFaces(left, true, true).Any(x => x.IsEqual(right));
        }

        /// <summary>
        /// Returns <c>true</c> if the type implements <paramref name="baseTypeOrIface"/> (if it is an interface) 
        /// or is a super type of <paramref name="baseTypeOrIface"/> (if it is a type).
        /// </summary>
        /// <remarks>
        /// <para>This method may throw an exception when on of the super types of 
        /// <paramref name="type"/> cannot be resolved by Cecil, because than it cannot 
        /// determine, whether <paramref name="type"/> implements 
        /// <paramref name="baseTypeOrIface"/>, or not.</para>
        /// <para>Instead of of <see cref="Implements(TypeReference, Type)"/>, one can 
        /// use <see cref="SafeImplements"/> which does not throw any exceptions.</para>
        /// <para>Try to avoid this method and use directly Cecil data structures.</para>
        /// </remarks>
        public static bool Implements(this TypeReference type, Type baseTypeOrIface)
        {
            return type.IsEqual(baseTypeOrIface) ||
                type.GetBaseTypesAndInterfaces().Any(t => t.IsEqual(baseTypeOrIface));
        }

        /// <summary>
        /// Safe version of <see cref="Implements(TypeReference, Type)"/> that does not throw exception, 
        /// instead it returns <c>false</c> when it cannot resolve on of the base types.
        /// </summary>
        public static bool SafeImplements(this TypeReference type, Type baseTypeOrIface)
        {
            if (type.IsEqual(baseTypeOrIface))
            {
                return true;
            }

            try
            {
                return type.GetBaseTypesAndInterfaces().Any(t => t.IsEqual(baseTypeOrIface));                
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Returns <c>true</c> if the type implements <paramref name="baseTypeOrIface"/> (if it is an interface) 
        /// or is a super type of <paramref name="baseTypeOrIface"/> (if it is a type).
        /// </summary>
        public static bool Implements(this TypeReference type, TypeReference baseTypeOrIface)
        {
            return type.GetBaseTypesAndInterfaces().Concat(new[] { type }).Any(t => t.IsEqual(baseTypeOrIface));
        }

        #endregion

        #region Generics

        /// <summary>
        /// If the type implements or inherits from given baseOfIface generic type, 
        /// then generic arguments of the baseOfIface are returned. 
        /// The type may possibly implement baseOfIface more times, in this case 
        /// the enumeration will contain concatenation of all generic arguments.
        /// </summary>
        /// <param name="type">The target type</param>
        /// <param name="baseOrIface">Generic base class or interface.</param>
        public static IEnumerable<TypeReference> GetGenericArgumentsFor(this TypeReference type, Type baseOrIface)
        {
            Contract.Requires(type != null);
            Contract.Requires(
                baseOrIface.IsGenericType && baseOrIface.IsGenericTypeDefinition, 
                "baseOfIface should be generic type definition e. g. typeof(List<>).");

            var result = new List<TypeReference>();
            var typesAndIfaces = GetBaseTypesExtension.GetBaseTypesAndIFaces(type, true, true).Concat(type);
            foreach (var baseType in typesAndIfaces)
            {
                if (baseType is GenericInstanceType == false) { continue; }
                if (baseType.IsGenericTypeEqual(baseOrIface) == false) { continue; }
                var generic = (GenericInstanceType)baseType;
                if (generic.GenericArguments.Count != baseOrIface.GetGenericArguments().Length) { continue; }
                result.AddRange(generic.GenericArguments);
            }

            return result;
        }

        /// <summary>
        /// Returns generic arguments of given type reference.
        /// This method always returns non null enumeration, but 
        /// if the type is not generic, this enumeration will be empty.
        /// </summary>
        public static IEnumerable<TypeReference> GetGenericArguments(this TypeReference type)
        {
            Contract.Ensures(Contract.Result<IEnumerable<TypeReference>>() != null);
            if (type is GenericInstanceType)
            {
                return Enumerable.Empty<TypeReference>();
            }

            return ((GenericInstanceType)type).GenericArguments;
        }

        /// <summary>
        /// Returns <c>true</c> if <paramref name="type"/> is instance of generic type 
        /// <paramref name="genericType"/> and if generic arguments of <paramref name="type"/> 
        /// match types in <paramref name="genericArguments"/>.
        /// </summary>
        public static bool IsGenericInstanceOf(
            this TypeReference type,
            Type genericType,
            params TypeReference[] genericArguments)
        {
            Contract.Requires(
                genericType.IsGenericTypeDefinition && genericType.IsGenericType,
                "Parameter genericType should be generic type definition e. g. typeof(List<>), not typeof(List<int>)");
            return type.IsGenericInstanceOf(genericType.GetTypeDefinition(), genericArguments);
        }

        /// <summary>
        /// Returns <c>true</c> if <paramref name="type"/> is instance of generic type 
        /// <paramref name="genericType"/> and if generic arguments of <paramref name="type"/> 
        /// match types in <paramref name="genericArguments"/>.
        /// </summary>
        public static bool IsGenericInstanceOf(
            this TypeReference type,
            TypeReference genericType,
            params TypeReference[] genericArguments)
        {
            Contract.Requires(type.IsGenericInstance, "IsGenericInstanceOf should be called on generic instances.");
            var genericTypeWithArguments = genericType.MakeGenericInstanceType(genericArguments);
            return genericTypeWithArguments.IsEqual(type);
        }

        #endregion

        #region Additional Information about types

        /// <summary>
        /// Returns <c>true</c> if <paramref name="type"/> could be considered as a static type in C#.
        /// </summary>
        /// <remarks>
        /// <para>
        /// Static keyword is not directly translated into MSIL. 
        /// However static classes are translated into abstract and sealed classes in MSIL.
        /// </para>
        /// </remarks>
        public static bool IsStaticType(this TypeDefinition type)
        {
            return type.Resolve().IsAbstract && type.Resolve().IsSealed;
        }

        #endregion

        #region Methods - overriding, aso.

        /// <summary>
        /// Returns all type methods plus all base types' methods. 
        /// Keep in mind that some of the returned methods may have <see cref="MethodDefinition.DeclaringType"/> set to 
        /// base type of the <paramref name="type"/> parameter.
        /// </summary>
        /// <remarks>
        /// <para>
        ///     Overridden methods are returned only once.
        /// </para>
        /// <para>
        ///     As well as Extension method GetMethods in Mono.Cecil.Rocks this method does not return constructors references, but 
        ///     extension method GetMethods from in Mono.Cecil.Rocks returns only methods declared in given type and 
        ///     not methods from base types.
        /// </para>
        /// </remarks>
        public static IEnumerable<MethodReference> GetAllMethods(this TypeDefinition type)
        {
            Contract.Requires(type != null);
            Contract.Ensures(Contract.Result<IEnumerable<MethodReference>>() != null);
            if (type.IsEqual(typeof(object)))
            {
                return type.GetMethods();
            }

            var current = type;
            var result = new List<MethodReference>();

            // These are methods that are overriden, so we dont want to include them in the result.
            var baseMethodToExclude = new List<MethodReference>();

            do
            {                
                result.AddRange(current.GetMethods().Where(m => baseMethodToExclude.None(baseMethod => m.IsEqual(baseMethod))));

                baseMethodToExclude.AddRange(
                    current.GetMethods().Select(m => m.GetBaseMethod()).Where(baseMethod => baseMethod != null));

                current = current.BaseType.SafeResolve(
                    string.Format(CannotResolveMessage, type.BaseType.FullName, type.FullName));
            }
            while (current.IsEqual(typeof(object)) == false);

            return result;
        }

        /// <summary>
        /// This is just shortcut for 
        /// <code>
        ///     Methods.First(x => x.Name == methodName)
        /// </code>
        /// </summary>
        public static MethodReference GetMethod(this TypeDefinition type, string methodName)
        {
            Contract.Requires(type != null);
            Contract.Ensures(Contract.Result<MethodReference>() != null);

            var result = type.Methods.Where(m => m.Name == methodName).FirstOrDefault();
            if (result == null)
            {
                var message = string.Format(
                    "TypeExtensions.GetMethod(string): Type {0} does not have a method {1}.", 
                    type.Name, 
                    methodName);
                throw new KeyNotFoundException(message);
            }

            return result;
        }

        /// <summary>
        /// Returns <c>true</c> if <paramref name="type"/> overrides the 
        /// method <paramref name="overridden"/> from base type or interface.
        /// </summary>
        public static bool Overrides(this TypeDefinition type, MethodReference overridden)
        {
            Contract.Requires(type != null);
            Contract.Requires(overridden != null);
            return type.GetMethods().Any(m => m.Overrides(overridden));
        }

        /// <summary>
        /// Returns all methods from <paramref name="type"/> that can be override in it's super type.
        /// </summary>
        public static IEnumerable<MethodReference> GetOverrideableMethods(this TypeDefinition type)
        {
            var baseType = type;
            var sealedMethodsBases = new List<MethodReference>();
            var result = new List<MethodReference>();

            do
            {
                baseType = baseType.BaseType.SafeResolve(
                    string.Format(CannotResolveMessage, baseType.BaseType.FullName, baseType.FullName));
                foreach (var method in baseType.GetMethods())
                {
                    if (method.IsOverriddeable())
                    {
                        // Now check that this method has not been sealed in any subclass
                        var baseMethod = method.GetOriginalBaseMethod();
                        if (sealedMethodsBases.None(sealedMethod => sealedMethod.IsEqual(baseMethod)) &&
                            result.None(m => m.IsEqual(baseMethod)))
                        {
                            result.Add(baseMethod);
                        }                        
                    }
                    else
                    {
                        // If it is not overrideable, but overrides some base method,
                        if (method.OverridesBaseMethod())
                        {
                            // then it makes this base method sealed
                            sealedMethodsBases.Add(method.GetOriginalBaseMethod());
                        }
                    }
                }
            }
            while (baseType.IsEqual(typeof(object)) == false);

            return result;
        }

        #endregion

        #region Equivalence

        /// <summary>
        /// Compares this <see cref="TypeReference"/> with another instances of <see cref="TypeReference"/>.
        /// </summary>
        public static bool IsEqual(this TypeReference typeReference, TypeReference typeReference2)
        {
            Contract.Requires(typeReference != null);
            Contract.Requires(typeReference2 != null);

            if (typeReference is TypeSpecification || typeReference2 is TypeSpecification)
            {
                if (typeReference.GetType() != typeReference2.GetType())
                {
                    return false;
                }

                return TypeMatch((TypeSpecification)typeReference, (TypeSpecification)typeReference2);
            }

            return typeReference.FullName.Equals(typeReference2.FullName, StringComparison.Ordinal);
        }

        /// <summary>
        /// Returns a full name of <paramref name="type"/> that is conforms 
        /// to <see cref="System.Reflection"/> conventions.
        /// </summary>
        /// <remarks>
        /// <para>This method is used for API documentation parsing in Architecture Explorer.</para>
        /// </remarks>
        public static string GetReflectionFullName(this TypeReference type)
        {
            return CecilToReflection.GetReflectionTypeName(type.FullName);
        }

        private static bool TypeMatch(TypeSpecification a, TypeSpecification b)
        {
            if (a is GenericInstanceType)
            {
                return TypeMatch((GenericInstanceType)a, (GenericInstanceType)b);
            }

            return a.ElementType.IsEqual(b.ElementType);
        }

        private static bool TypeMatch(GenericInstanceType a, GenericInstanceType b)
        {
            if (a.ElementType.IsEqual(b.ElementType) == false)
            {
                return false;
            }

            if (a.GenericArguments.Count != b.GenericArguments.Count)
            {
                return false;
            }

            if (a.GenericArguments.Count == 0)
            {
                return true;
            }

            for (int i = 0; i < a.GenericArguments.Count; i++)
            {
                if (a.GenericArguments[i].IsEqual(b.GenericArguments[i]) == false)
                {
                    return false;
                }
            }

            return true;
        }

        #endregion
    }
}
